package org.jspresso.tester.utils;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.hibernate.SQLQuery;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Restrictions;
import org.json.JSONArray;
import org.json.JSONObject;
import org.jspresso.framework.action.ActionBusinessException;
import org.jspresso.framework.application.backend.persistence.hibernate.HibernateBackendController;
import org.jspresso.framework.application.backend.session.EMergeMode;
import org.jspresso.framework.model.component.IComponent;
import org.jspresso.framework.model.descriptor.ICollectionPropertyDescriptor;
import org.jspresso.framework.model.descriptor.IComponentDescriptor;
import org.jspresso.framework.model.descriptor.IDatePropertyDescriptor;
import org.jspresso.framework.model.descriptor.IPropertyDescriptor;
import org.jspresso.framework.model.descriptor.IReferencePropertyDescriptor;
import org.jspresso.framework.model.descriptor.IScalarPropertyDescriptor;
import org.jspresso.framework.model.descriptor.ITimePropertyDescriptor;
import org.jspresso.framework.model.descriptor.basic.AbstractEnumerationPropertyDescriptor;
import org.jspresso.framework.model.descriptor.basic.BasicBooleanPropertyDescriptor;
import org.jspresso.framework.model.descriptor.basic.BasicColorPropertyDescriptor;
import org.jspresso.framework.model.descriptor.basic.BasicComponentDescriptor;
import org.jspresso.framework.model.descriptor.basic.BasicDatePropertyDescriptor;
import org.jspresso.framework.model.descriptor.basic.BasicDecimalPropertyDescriptor;
import org.jspresso.framework.model.descriptor.basic.BasicDurationPropertyDescriptor;
import org.jspresso.framework.model.descriptor.basic.BasicIntegerPropertyDescriptor;
import org.jspresso.framework.model.descriptor.basic.BasicListDescriptor;
import org.jspresso.framework.model.descriptor.basic.BasicObjectPropertyDescriptor;
import org.jspresso.framework.model.descriptor.basic.BasicStringPropertyDescriptor;
import org.jspresso.framework.model.descriptor.basic.BasicTimePropertyDescriptor;
import org.jspresso.framework.model.entity.IEntity;
import org.jspresso.framework.model.entity.IEntityFactory;
import org.jspresso.framework.util.accessor.IAccessor;
import org.jspresso.model.TestEntity;
import org.jspresso.model.TestScope;
import org.jspresso.model.extension.TestScopeExtension;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;

public class TestEntitySerializer {

	public static void serializeToStore(final IComponent component, final TestScope scope, final boolean isGeneratedEntity, final boolean isResultEntity, final HibernateBackendController controller) {
		controller.getTransactionTemplate().execute(new TransactionCallbackWithoutResult() {

			@Override
			protected void doInTransactionWithoutResult(TransactionStatus arg0) {
				TestScope txScope = null;
				if(scope == null) {
					txScope = getDefaultScope(controller);
				}
				else {
					txScope = controller.cloneInUnitOfWork(scope);
				}
				storeSerializedComponents(serialize(component, controller), txScope, isGeneratedEntity, isResultEntity, controller);				
			}
		});

	}

	public static void storeSerializedComponents(HashMap<Class<? extends IComponent>, Map<Serializable, String>> serialMap, TestScope scope, boolean isGeneratedEntity, final boolean isResultEntity, HibernateBackendController controller) {
		for(Class<? extends IComponent> clazz : serialMap.keySet()) {
			Map<Serializable, String> clazzMap = serialMap.get(clazz);
			for(Serializable id : clazzMap.keySet()) {
				TestEntity existingEntity = findTestEntity(id, clazz, scope, controller);
				if(existingEntity == null) {
					existingEntity = controller.getEntityFactory().createEntityInstance(TestEntity.class);
					existingEntity.setEntityId(id);
					existingEntity.setClazzName(clazz.getCanonicalName());
					existingEntity.setTestScope(scope);
					existingEntity.setGeneratedEntity(isGeneratedEntity);
					existingEntity.setResultEntity(isResultEntity);
				}
				String serializedObject = clazzMap.get(id);
				existingEntity.setEntitySerialized(serializedObject);
				controller.registerForUpdate(existingEntity);
			}
		}
	}

	@SuppressWarnings("unchecked")
	public static IComponent unserializeFromStore(Serializable id, Class<? extends IComponent> clazz, TestScope scope, HibernateBackendController controller) {
		IComponent entity = null;
		if(scope == null) {
			scope = getDefaultScope(controller);
		}
		TestEntity testEntity = findTestEntity(id, clazz, scope, controller);
		if(testEntity != null) {
			if(IEntity.class.isAssignableFrom(clazz)) {
				// Do we already have it in session ?
				IEntity alreadyLoaded = controller.findById(id, EMergeMode.MERGE_KEEP, (Class<? extends IEntity>)clazz);
				if(alreadyLoaded != null) {
					entity = alreadyLoaded;
				}
				else {
					entity = controller.getEntityFactory().createEntityInstance(testEntity.getClazz(), testEntity.getEntityId());
					if(!controller.isUnitOfWorkActive()) {
						entity = controller.merge((IEntity)entity, EMergeMode.MERGE_KEEP);
					}
					JSONObject json = new JSONObject(testEntity.getEntitySerialized());
					hydrayteComponentFromJson(entity, json, scope, controller);
				}
			}
			else {
				// Cannot look into session... We'll load it as a unique IComponent
				entity = controller.getEntityFactory().createComponentInstance(testEntity.getClazz());
				JSONObject json = new JSONObject(testEntity.getEntitySerialized());
				hydrayteComponentFromJson(entity, json, scope, controller);
			}
		}
		return entity;
	}

	@SuppressWarnings("unchecked")
	private static void hydrayteComponentFromJson(IComponent component, JSONObject json, TestScope scope, HibernateBackendController controller) {

		IComponentDescriptor<?> descriptor = controller.getEntityFactory().getComponentDescriptor(component.getComponentContract());
		for(IPropertyDescriptor propertyDescriptor : (Collection<IPropertyDescriptor>)(descriptor.getPropertyDescriptors())) {
			Object propertyValue = null;
			if(!propertyDescriptor.isComputed() && !propertyDescriptor.getName().equals(IEntity.ID)) {
				if(propertyDescriptor instanceof IScalarPropertyDescriptor) {
					String propertyStringValue = (String)(json.get(propertyDescriptor.getName()));
					propertyValue = convertToAppropriateType(propertyStringValue, propertyDescriptor);
				}

				else 
					if(propertyDescriptor instanceof IReferencePropertyDescriptor) {

						IComponentDescriptor<? extends IComponent> referencedDescriptor = ((IReferencePropertyDescriptor<? extends IComponent>) propertyDescriptor).getReferencedDescriptor();

//						if(referencedDescriptor instanceof BasicComponentDescriptor<?>) {
//							JSONObject propertyJson = (JSONObject)(json.get(propertyDescriptor.getName()));
//							propertyValue = controller.getEntityFactory().createComponentInstance(referencedDescriptor.getComponentContract());
//							hydrayteComponentFromJson((IComponent)propertyValue, propertyJson, scope, controller);
//						}
//						else { // Should be referencing an Entity
							propertyValue = unserializeFromStore((Serializable) json.get(propertyDescriptor.getName()), referencedDescriptor.getComponentContract(), scope, controller);
//						}
					}
					else if(propertyDescriptor instanceof ICollectionPropertyDescriptor) {

						JSONArray referencedEntities = (JSONArray) json.get(propertyDescriptor.getName());
						if(((ICollectionPropertyDescriptor<?>)propertyDescriptor).getCollectionDescriptor() instanceof BasicListDescriptor<?>) {
							propertyValue = new ArrayList<Object>();
						}
						else {
							propertyValue = new HashSet<Object>();
						}
						for(int i=0; i<referencedEntities.length(); i++) {
							String referencedId = (String) referencedEntities.get(i);
							Object itemValue = unserializeFromStore((Serializable) referencedId, (Class<? extends IComponent>) ((ICollectionPropertyDescriptor<?>)propertyDescriptor).getCollectionDescriptor().getElementDescriptor().getComponentContract(), scope, controller);
							((Collection<Object>)propertyValue).add(itemValue);
						}
					}

				try {
					if(propertyDescriptor.getName().equals(IEntity.VERSION)) {
						component.straightSetProperty(IEntity.VERSION, propertyValue);
					}
					else {
						if(propertyValue instanceof Collection<?>) {
							IAccessor propertyAccessor = controller.getAccessorFactory().createCollectionPropertyAccessor(propertyDescriptor.getName(), component.getComponentContract(), ((ICollectionPropertyDescriptor<?>)propertyDescriptor).getCollectionDescriptor().getElementDescriptor().getComponentContract());
							propertyAccessor.setValue(component, propertyValue);
						}
						else {
							IAccessor propertyAccessor = controller.getAccessorFactory().createPropertyAccessor(propertyDescriptor.getName(), component.getComponentContract());
							propertyAccessor.setValue(component, propertyValue);
						}
					}
				} catch (IllegalAccessException e) {
					e.printStackTrace();
				} catch (InvocationTargetException e) {
					e.printStackTrace();
				} catch (NoSuchMethodException e) {
					e.printStackTrace();
				}
			}
		}
	}


	public static HashMap<Class<? extends IComponent>, Map<Serializable, String>> serialize(IComponent component, HibernateBackendController controller) {
		HashMap<Class<? extends IComponent>, Map<Serializable, String>> serialMap = new HashMap<Class<? extends IComponent>, Map<Serializable, String>>();
		serializePropertyComponent(component, serialMap, controller);
		return serialMap;
	}


	private static Object convertToAppropriateType(String representation, IPropertyDescriptor descriptor) {
		Object obj = null;
		if(representation != null && !representation.equals("null")) {
			if(descriptor instanceof BasicStringPropertyDescriptor || descriptor instanceof AbstractEnumerationPropertyDescriptor || descriptor instanceof BasicColorPropertyDescriptor || descriptor instanceof BasicObjectPropertyDescriptor) {
				obj = representation;
			}
			else if(descriptor instanceof BasicDecimalPropertyDescriptor) {
				if(((BasicDecimalPropertyDescriptor)descriptor).isUsingBigDecimal()) {
					obj = new BigDecimal(representation);
				}
				else {
					obj = new Double(representation);
				}
			}
			else if(descriptor instanceof BasicIntegerPropertyDescriptor) {
				obj = new Integer(representation);
			}
			else if(descriptor instanceof BasicDurationPropertyDescriptor) {
				obj = new Long(representation);
			}
			else if(descriptor instanceof BasicDatePropertyDescriptor || descriptor instanceof BasicTimePropertyDescriptor) {
				obj = new Date(Long.parseLong(representation));
			}
			else if(descriptor instanceof BasicBooleanPropertyDescriptor) {
				obj = new Boolean(representation);
			}
		}	
		return obj;
	}

	public static TestScope getDefaultScope(final HibernateBackendController controller) {
		DetachedCriteria scopeCriteria = DetachedCriteria.forClass(TestScope.class);
		scopeCriteria.add(Restrictions.eq(TestScope.USER_NAME, controller.getApplicationSession().getUsername().equals("Unknown user" ) ? "demo" : controller.getApplicationSession().getUsername() ));
		scopeCriteria.add(Restrictions.eq(TestScope.SCOPE_ID, TestScopeExtension.DEFAULT_SCOPE));
		TestScope defaultScope = controller.findFirstByCriteria(scopeCriteria, EMergeMode.MERGE_CLEAN_EAGER, TestScope.class);
		if(defaultScope == null) {
			defaultScope = controller.getTransactionTemplate().execute(new TransactionCallback<TestScope>() {
				@Override
				public TestScope doInTransaction(TransactionStatus status) {
					TestScope createdScope = controller.getEntityFactory().createEntityInstance(TestScope.class);
					createdScope.setUserName(controller.getApplicationSession().getUsername().equals("Unknown user" ) ? "demo" : controller.getApplicationSession().getUsername());
					createdScope.setScopeId(TestScopeExtension.DEFAULT_SCOPE);
					controller.registerForUpdate(createdScope);
					return createdScope;
				}
			});
		}
		return defaultScope;
	}

	public static TestEntity findTestEntity(Serializable id, Class<? extends IComponent> clazz, TestScope scope, HibernateBackendController controller) {
		if(scope == null) {
			scope = getDefaultScope(controller);
		}
		DetachedCriteria testEntityCriteria = DetachedCriteria.forClass(TestEntity.class);
		testEntityCriteria.add(Restrictions.eq(TestEntity.CLAZZ_NAME, clazz.getCanonicalName()));
		testEntityCriteria.add(Restrictions.eq(TestEntity.ENTITY_ID, id));
		testEntityCriteria.add(Restrictions.eq(TestEntity.TEST_SCOPE, scope));
		return controller.findFirstByCriteria(testEntityCriteria, EMergeMode.MERGE_CLEAN_EAGER, TestEntity.class);
	}

	@SuppressWarnings("unchecked")
	public static TestEntity findTestEntity(TestEntity templateTestEntity, TestScope scope, HibernateBackendController controller) {
		if(templateTestEntity == null) {
			return null;
		}
		return findTestEntity(templateTestEntity.getEntityId(), (Class<? extends IComponent>)templateTestEntity.getClazz(), scope, controller);
	}


	private static String serializePropertyCollection(Collection<IComponent> col, HashMap<Class<? extends IComponent>, Map<Serializable, String>> serialMap, HibernateBackendController controller) {
		boolean firstItem = true;
		Iterator<IComponent> collectionIt = col.iterator();
		String serializedProperty = "[";
		while(collectionIt.hasNext()) {
			if(firstItem) {
				firstItem = false; 
			}
			else {
				serializedProperty += ",";
			}
			Object colObj = collectionIt.next();
			if(colObj instanceof IEntity) {
				serializedProperty += "\"" + ((IEntity) colObj).getId().toString() + "\"";
			}
			else { // This is a list of IComponent
				serializedProperty += "\"" + String.valueOf(((IComponent) colObj).hashCode()) + "\"";
			}
			serializePropertyComponent(((IComponent) colObj), serialMap, controller);
		}
		serializedProperty += "]";
		return serializedProperty;
	}

	@SuppressWarnings("unchecked")
	private static String serializePropertyComponent(IComponent component, HashMap<Class<? extends IComponent>, Map<Serializable, String>> serialMap, HibernateBackendController controller) {
		IEntityFactory entityFactory = controller.getEntityFactory();
		IComponentDescriptor<?> componentDescriptor = entityFactory.getComponentDescriptor(component.getComponentContract());
		String serialized = "";

		for (IPropertyDescriptor propertyDescriptor : componentDescriptor.getPropertyDescriptors()) {
			if(!propertyDescriptor.isComputed()) {
				String propertyName = propertyDescriptor.getName();
				Object propertyValue = null;

				try {
					if(propertyDescriptor instanceof ICollectionPropertyDescriptor<?>) {
						IAccessor propertyAccessor = controller.getAccessorFactory().createCollectionPropertyAccessor(propertyDescriptor.getName(), component.getComponentContract(), ((ICollectionPropertyDescriptor<?>)propertyDescriptor).getCollectionDescriptor().getElementDescriptor().getComponentContract());
						propertyValue = propertyAccessor.getValue(component);
					}
					else {
						IAccessor propertyAccessor = controller.getAccessorFactory().createPropertyAccessor(propertyDescriptor.getName(), component.getComponentContract());
						propertyValue = propertyAccessor.getValue(component);
					}
				}
				catch(Exception e) {
					e.printStackTrace();
					throw new ActionBusinessException(e.getMessage() + " - See logs");
				}

				String serializedProperty = null;

				if(propertyValue != null) {
					if(propertyDescriptor instanceof IScalarPropertyDescriptor) {

						if(propertyDescriptor instanceof IDatePropertyDescriptor || propertyDescriptor instanceof ITimePropertyDescriptor) {
							serializedProperty =  "\"" + propertyName + "\":\"" + ((Date)propertyValue).getTime() + "\"";
						}
						else if(!propertyName.equals(IEntity.ID)) {
							serializedProperty =  "\"" + propertyName + "\":\"" + propertyValue.toString() + "\"";
						}
					}
					else if(propertyDescriptor instanceof IReferencePropertyDescriptor) {
						if(propertyValue instanceof IEntity) {
							serializedProperty = "\"" + propertyName + "\":\"" + ((IEntity) propertyValue).getId().toString() + "\"";
							serializePropertyComponent(((IEntity) propertyValue), serialMap, controller);
						}
						else { // Should be a IComponent
							serializedProperty = "\"" + propertyName + "\":\"" + String.valueOf(propertyValue.hashCode()) + "\""; // We will use the hashCode for IComponent and use it as its ID
							serializePropertyComponent((IComponent)propertyValue, serialMap, controller);
							//							serializedProperty = "\"" + propertyName + "\":{" + serializePropertyComponent((IComponent) propertyValue, serialMap, controller) + "}";
						}
					}
					else if(propertyDescriptor instanceof ICollectionPropertyDescriptor) {
						serializedProperty = "\"" + propertyName + "\":" + serializePropertyCollection((Collection<IComponent>) propertyValue, serialMap, controller);
					}
				}


				if(!propertyName.equals(IEntity.ID)) {
					if(!serialized.isEmpty()) {
						serialized += ",";
					}
					serialized += serializedProperty != null ? serializedProperty.toString() : "\"" + propertyName + "\":\"" + serializedProperty + "\"";
				}
			}
		}

		Class<? extends IComponent> propertyClazz = component.getComponentContract();
		if(!serialMap.containsKey(propertyClazz)) {
			serialMap.put(propertyClazz, new HashMap<Serializable, String>());
		}

		if(component instanceof IEntity) { 
			serialMap.get(propertyClazz).put(((IEntity)component).getId(), "{" + serialized + "}");
		}
		else { // We will use the hashCode for IComponent and use it as its ID
			serialMap.get(propertyClazz).put(String.valueOf(component.hashCode()), "{" + serialized + "}");
		}


		return  serialized;
	}

	public static void cleanOrphanGeneratedTestEntities(TestScope scope, HibernateBackendController controller) {
		TestScope localScope = scope;
		if(localScope == null) {
			localScope = getDefaultScope(controller);
		}
		String sqlSelectQuery = "select * from TEST_ENTITY where GENERATED_ENTITY=1 and TEST_SCOPE_ID=:localScopeId and ID not in (select PARAMETER_REF_VALUE_ID from ACTION_TEST_IO) and ID not in (select TEST_ENTITY_ID from ACTION_TEST_IO_PARAMETER_REF_VALUES)";
		SQLQuery query = (SQLQuery) controller.getHibernateSession().createSQLQuery(sqlSelectQuery).addEntity(TestEntity.class).setParameter("localScopeId", localScope.getId().toString());
		List<TestEntity> testEntitiesToDelete = query.list();
		for(TestEntity testEntityToDelete : testEntitiesToDelete) {
			controller.registerForDeletion(testEntityToDelete);
		}
	}

}
